// Copyright (C) 2010, Scott W. Dunlop <swdunlop@gmail.com>
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//    * Redistributions of source code must retain the above copyright
//      notice, this list of conditions and the following disclaimer.
//    * Redistributions in binary form must reproduce the above copyright
//      notice, this list of conditions and the following disclaimer in the
//      documentation and/or other materials provided with the distribution.
//    * Neither the name of the <organization> nor the
//      names of its contributors may be used to endorse or promote products
//      derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
// ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#define NOSPEX_IMPL_SOURCE // Indicates that this is an implementation file.
#define NX_USE_PTHREADS // Use Pthreads instead of rigging reentrant mutexes.

#include "nospex.h"
#include "sdl.h"
#include "util.h"
#include <SDL.h>

#include <assert.h>
#include <stdlib.h>

#ifdef NX_USE_PTHREADS
#include <pthread.h>
#include <unistd.h>
#endif

#define VIDEO_FLAGS \
    ( SDL_RESIZABLE | SDL_HWSURFACE | SDL_DOUBLEBUF )

#define EI0( doing, expr ) if( !(expr) ) nx_sdl_error( doing );
#define EIN( doing, expr ) if( (expr) < 0 ) nx_sdl_error( doing );

void nx_sdl_error( char* doing ){
    fprintf( stderr, "SDL Error while %s: %s\n", doing, SDL_GetError() );
    exit(1);
}

void nx_delete_window_sdl_surface( nx_window_p window ){
    if( ! window->sdl_surface ) return;
    SDL_FreeSurface( window->sdl_surface );
    window->sdl_surface = NULL;
    //TODO: Locking?
};

nx_window_p the_window = NULL;

void nx_create_window_sdl_surface( nx_window_p window, int width, int height ){
    if( window->sdl_surface != NULL ) nx_delete_window_sdl_surface( window );
    EI0( "setting video mode", 
         window->sdl_surface = SDL_SetVideoMode( 
             width, height, 32, VIDEO_FLAGS 
         ) );
    SDL_WM_SetCaption( "nospex", "nospex" );
}

void nx_delete_window_cairo_surface( nx_window_p window ){
    if( ! window->cairo_surface ) return;
    cairo_surface_destroy( window->cairo_surface );
    window->cairo_surface = NULL;
}

void nx_create_window_cairo_surface( nx_window_p window ){
    nx_delete_window_cairo_surface( window );
    
    window->cairo_surface = cairo_image_surface_create_for_data( 
        (void*)(window->sdl_surface->pixels),
        CAIRO_FORMAT_RGB24,
        window->sdl_surface->w,
        window->sdl_surface->h,
        window->sdl_surface->pitch
    );
};

void nx_update_window_size( nx_window_p window, int w, int h ){
    if( ! window->graph ) return;
    nx_create_window_sdl_surface( window, w, h );
    nx_create_window_cairo_surface( window );
    nx_resize_graph( window->graph, w, h );     
}

void nx_update_window_display( nx_window_p window ){
    if( ! window->graph ) return;
    while( SDL_LockSurface( window->sdl_surface ) ) SDL_Delay( 1 );

    cairo_t* cr = cairo_create( window->cairo_surface );
    nx_draw_graph( window->graph, cr );
    cairo_surface_flush( window->cairo_surface );
    cairo_destroy( cr );

    SDL_UnlockSurface( window->sdl_surface );
    SDL_Flip( window->sdl_surface );
};

static int sdl_initialized = 0;
#ifdef NX_USE_PTHREADS
static pthread_mutex_t nx_control;
static pthread_mutexattr_t nx_control_attr;
#else
static SDL_mutex* nx_control_outer;      // Only one thread can check control.
static SDL_sem* nx_control_inner;        // Only one thread has control.
static Uint32 nx_control_thread;         // What thread has control?
static Uint32 nx_control_count;          // How deep is the current control?
#endif

void nx_initialize_sdl(  ){
    if( sdl_initialized  )return;
    sdl_initialized = 1;
//atexit( SDL_Quit );
    EIN( "initializing SDL", 
         SDL_Init( SDL_INIT_VIDEO | SDL_INIT_TIMER ) );
#ifdef NX_USE_PTHREADS
    pthread_mutexattr_init( &nx_control_attr );
    pthread_mutexattr_settype( &nx_control_attr, PTHREAD_MUTEX_RECURSIVE );
    pthread_mutex_init( &nx_control, &nx_control_attr );
#else
    nx_control_outer = SDL_CreateMutex( );
    nx_control_inner = SDL_CreateSemaphore( 0 );
    nx_control_thread = 0;
    nx_control_count = 0;
#endif
}

/** Interrupts the SDL thread. */
void nx_seize_control( ){
    //printf( "Seizing control.\n" );
#ifdef NX_USE_PTHREADS
    pthread_mutex_lock( &nx_control );
#else
    Uint32 self = SDL_ThreadID( );
    printf( "Seizing thread %x seizing control..\n", self );
    for(;;){
        printf( "..Seizing thread %x locking outer control..\n", self );
        EIN( "acquiring outer control for seize",
             SDL_LockMutex( nx_control_outer ) );
        printf( "..Seizing thread %x has acquired outer control..\n", self );
        if( ! nx_control_count ){
            printf( "..No count, setting myself as controller..\n" );
            nx_control_thread = self; nx_control_count = 1; break;
        }else if( self == nx_control_thread ){
            printf( "..I have control, increasing count..\n" );
            nx_control_count ++; break;
        }else{
            printf( "..I don't have control, waiting on condition..\n" );
            printf( "  (( %x has control, %i is the count. ))\n", nx_control_thread, nx_control_count );
            EIN( "releasing outer control before seize", 
                 SDL_UnlockMutex( nx_control_outer ) );
            EIN( "waiting on inner control", 
                 SDL_SemWait( nx_control_inner ) );
            printf( "..Seizing thread %x has stopped waiting on the condition..\n", self );
        }
    }
    printf( "..Seizing thread %x releasing outer control.\n", self );
    EIN( "releasing outer control after seize", 
         SDL_UnlockMutex( nx_control_outer ) );
#endif
    //printf( "Control seized.\n" );
};

/** Releases the SDL thread. */
void nx_release_control( ){
    //printf( "Released control.\n" );
#ifdef NX_USE_PTHREADS
    pthread_mutex_unlock( &nx_control );
#else
    Uint32 self = SDL_ThreadID( );
    printf( "Releasing thread %x releasing control..\n", self );
    printf( "..Releasing thread %x locking outer control..\n", self );
    EIN( "acquiring outer control for release",
         SDL_LockMutex( nx_control_outer ) );
    printf( "..I have control, checking assertions..\n", self );
    assert( nx_control_thread == self );
    assert( nx_control_count > 0 );
    printf( "..Decreasing control..\n", self );
    nx_control_count --;
    printf( ".. Count is now %i..\n", nx_control_count );
    if( ! nx_control_count ){
        printf( "..Signalling inner condition..\n", self );
        nx_control_thread = 0;
        EIN( "signalling inner condition",
             SDL_CondSignal( nx_control_inner ) );
    };
    printf( "..Releasing thread %x releasing outer control.\n", self );
    EIN( "releasing outer control after release",
         SDL_UnlockMutex( nx_control_outer ) );
#endif
};

nx_window_p nx_create_window( nx_graph_p graph ){
    nx_initialize_sdl( );
    nx_window_p window = (nx_window_p)nx_create_resource( 
        NX_GRAPH_KIND, sizeof( struct nx_window_t ) 
    );
    window->graph = graph;
    graph->window = window;
    nx_create_window_sdl_surface( window, 600, 600 );
    nx_create_window_cairo_surface( window );
    the_window = window;
    return window;

};

void nx_unlink_window( nx_window_p window ){
    nx_delete_window_cairo_surface( window );
    nx_delete_window_sdl_surface( window );
    //TODO
};

enum {
    NX_RENDER_EVENT,
    NX_PHYSICS_EVENT,
    NX_BLIPS_EVENT
};


void nx_request_graph_render( nx_graph_p graph ){
    if( ! graph->window )return;
    graph->render_req = NX_RUNNING;
}

void nx_schedule_graph_blips( nx_graph_p graph ){
    if( ! graph->window )return;
    graph->blips_req = NX_RUNNING;
}

Uint32 nx_thump_sdl( Uint32 interval, void* ignored ){
    // This ensures that the SDL event loop cycles every 30ms.
    SDL_Event event;
    event.type = SDL_USEREVENT;
    event.user.code = 0; 
    SDL_PushEvent( &event );
    return interval;
};

void nx_schedule_graph_physics( nx_graph_p graph ){ 
    SDL_Event event;
    if( graph->physics_req == NX_RUNNING ) return;
    graph->physics_req = NX_RUNNING;
    nx_thump_sdl( 0, NULL );
}

void nx_window_loop( nx_window_p window ){
    double x, y;
    SDL_Event event;
    SDL_TimerID thumper = SDL_AddTimer( 30, nx_thump_sdl, NULL );
    nx_time next_render = 0;
    
    // Precautionary render.
    nx_update_window_display( window );
    for(;;){
        //printf( "Seizing control at start of cycle.\n" );
        nx_seize_control( );
        nx_time now = nx_system_clock( );
        if( window->graph == NULL ) goto done;
        if( next_render <= now ){
            if(( window->graph->render_req )||( window->graph->blips_req )){
                window->graph->render_req = 0;
                nx_update_window_display( window ); 
                next_render = nx_system_clock( ) + 30;
            };
        };

        //While physics are active, we don't sleep, we just quickly check
        //poll.

        if( window->graph->physics_req == NX_RUNNING ){
            nx_apply_graph_physics( window->graph );
            //printf( "Releasing control before poll.\n" );
            nx_release_control( );
            SDL_PollEvent( &event );
            //printf( "Seizing control after poll.\n" );
            nx_seize_control( );
        }else{
            //printf( "Releasing control before wait.\n" );
            nx_release_control( );
            SDL_WaitEvent( &event );
            //while( SDL_PollEvent( &event ) ) usleep( 1000 );
            //printf( "Seizing control after wait.\n" );
            nx_seize_control( );
        };
    
        switch( event.type ){
        case SDL_VIDEOEXPOSE:
            nx_update_window_display( window );
            break;
        case SDL_MOUSEMOTION:
            nx_convert_to_model_xy( window->graph, &x, &y );
            x = event.motion.x; y = event.motion.y;
            nx_convert_to_model_xy( window->graph, &x, &y );
            nx_graph_handle_motion( window->graph, x, y );
            break;
        case SDL_MOUSEBUTTONDOWN:
            x = event.button.x; y = event.button.y;
            nx_convert_to_model_xy( window->graph, &x, &y );
            nx_graph_handle_button_press( window->graph, x, y );
            break;
        case SDL_MOUSEBUTTONUP:
            x = event.button.x; y = event.button.y;
            nx_convert_to_model_xy( window->graph, &x, &y );
            nx_graph_handle_button_release( window->graph, x, y );
            break;
        case SDL_VIDEORESIZE:
            nx_update_window_size( window, event.resize.w, event.resize.h );
            break;
        case SDL_QUIT:
            //printf( "Quit detected.\n");
            goto done;
        case SDL_USEREVENT: // Probably a thump.
            break;
        };
        //printf( "Releasing control at end of cycle.\n" );
        nx_release_control( );
    };

done:
    //printf( "Releasing control since cycle is done.\n" );
    nx_release_control( );
    SDL_RemoveTimer( thumper );
}

void nx_background_loop( ){
    SDL_CreateThread( (void*)nx_window_loop, the_window );
};

